/*
 * Copyright (c) 2007 Compl Yue Still, All Rights Reserved.
 */

package hbi.wire.java.host;

import hbi.wire.ProtocolError;

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ResolvedType
{

    private static final Method OVERLOADED;
    static
    {
        try
        {
            // Object.clone() is protected and available in every JVM, ideal to
            // be our marker constant
            OVERLOADED = Object.class.getDeclaredMethod("clone");
        }
        catch (Exception e)
        {
            throw new Error(e);
        }
    }

    private static boolean isOverwriting(Method m1, Method m2)
    {
        assert m1.getName().equals(m2.getName()); // or it won't get here
        if (m1.getDeclaringClass() == m2.getDeclaringClass())
        // can not be overwriting at the same class
            return false;
        return Arrays.equals(m1.getParameterTypes(), m2.getParameterTypes());
    }

    private static boolean appendTypeTag(StringBuilder tags, Class<?> type)
    {
        if (type.isArray())
        {
            Class<?> componentType = type.getComponentType();
            if (componentType == Byte.TYPE)
            {
                tags.append('B');
            }
            else
            {
                tags.append('[');
                return appendTypeTag(tags, componentType);
            }
        }
        else if (type == Object.class)
            tags.append('*');
        else if (type == Boolean.TYPE)
            tags.append('T');
        else if (type == Integer.TYPE)
            tags.append('I');
        else if (type == Long.TYPE)
            tags.append('L');
        else if (type == Double.TYPE)
            tags.append('D');
        else if (type == String.class)
            tags.append('S');
        else if (type == Date.class)
            tags.append('d');
        else if (type == Reader.class)
            tags.append('s');
        else if (type == InputStream.class)
            tags.append('b');
        else
            return false;

        return true;
    }

    protected final Class<?> javaClass;

    private Class<?> nextParse;

    protected ResolvedType(Class<?> javaClass)
    {
        this.javaClass = javaClass;
        this.nextParse = javaClass;
    }

    private final Map<String, Method> methodMap = new HashMap<String, Method>();

    private boolean parseMoreMethods()
    {
        Class<?> thisParse = this.nextParse;
        if (thisParse == null) return false;

        method_scan: for (Method m : thisParse.getDeclaredMethods())
        {
            if (m.isSynthetic()) continue;
            int mods = m.getModifiers();
            if (Modifier.isStatic(mods) || !Modifier.isPublic(mods)) continue;

            Class<?>[] paramTypes = m.getParameterTypes();
            StringBuilder typeTags = new StringBuilder(paramTypes.length);
            for (Class<?> pt : paramTypes)
            {
                if (!appendTypeTag(typeTags, pt))
                // contains unsupported parameter type
                    continue method_scan;
            }

            String name = m.getName();

            { // simple name
                Method existing = methodMap.get(name);
                if (existing == null)
                {
                    methodMap.put(name, m);
                }
                else
                {
                    if (existing != OVERLOADED && !isOverwriting(existing, m))
                        methodMap.put(name, OVERLOADED);
                }
            }

            { // mangled name with number of arguments
                String numberedName = name + '#' + paramTypes.length;
                Method existing = methodMap.get(numberedName);
                if (existing == null)
                {
                    methodMap.put(numberedName, m);
                }
                else
                {
                    if (existing != OVERLOADED && !isOverwriting(existing, m))
                        methodMap.put(numberedName, OVERLOADED);
                }
            }

            { // mangled name with argument type tag list
                String typedName = name + '#' + typeTags;
                Method existing = methodMap.get(typedName);
                if (existing == null)
                {
                    methodMap.put(typedName, m);
                }
                else
                {
                    // Java doesn't allow method overloading with same number of
                    // parameters and same types, so it must be overwriting
                    assert isOverwriting(existing, m);
                }
            }

            if (typeTags.indexOf("[") >= 0)
            { // contains array parameter, map mangled name regarding
                // unknown array type tags
                typeTags.setLength(0);
                for (Class<?> pt : paramTypes)
                {
                    if (pt.isArray())
                    {
                        if (pt.getComponentType() == Byte.TYPE)
                            typeTags.append('B');
                        else
                            typeTags.append(']'); // unknown array type
                    }
                    else
                    {
                        boolean appended = appendTypeTag(typeTags, pt);
                        assert appended;
                    }
                }
                String typedNameWithArray = name + '#' + typeTags;
                Method existing = methodMap.get(typedNameWithArray);
                if (existing == null)
                {
                    methodMap.put(typedNameWithArray, m);
                }
                else
                {
                    if (existing != OVERLOADED && !isOverwriting(existing, m))
                        methodMap.put(typedNameWithArray, OVERLOADED);
                }
            }
        }

        thisParse = this.nextParse = thisParse.getSuperclass();
        return thisParse != null;
    }

    private final List<Method> resolvedMethods = new ArrayList<Method>();

    protected void resolveMethod(String methodName) throws IOException
    {
        try
        {
            /*
             * algorithm here can't detect method overloading among a subclass
             * and its ancestor classes, however this seems a desirable side
             * effect.
             */
            Method method = methodMap.get(methodName);
            while (method == null)
            {
                int oldCount = methodMap.size();
                boolean hasMoreMethods2Parse = parseMoreMethods();
                if (methodMap.size() > oldCount) // only when the map changed
                    method = methodMap.get(methodName);
                if (!hasMoreMethods2Parse) break;
            }
            if (method == null)
                throw new ProtocolError("Unresolvable method: " + methodName);
            if (method == OVERLOADED)
                throw new ProtocolError("Ambiguous overloaded method: "
                    + methodName);
            this.resolvedMethods.add(method);
        }
        catch (Exception e)
        {
            throw new ProtocolError("Failed resolving method: " + methodName, e);
        }
    }

    protected Method getResolvedMethod(int methodIndex) throws ProtocolError
    {
        try
        {
            Method method = this.resolvedMethods.get(methodIndex);
            if (method == null)
                throw new ProtocolError("Invalid Method @ " + methodIndex);
            return method;
        }
        catch (Exception e)
        {
            throw new ProtocolError(e);
        }
    }

}
